/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, ts } from "arkanalyzer/lib";
import { AdviceChecker, BaseMetaData } from "../BaseChecker";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { AstMatcher, MatcherCallback, MatcherTypes } from "../../Index";
import { RuleFix } from "../../model/Fix";


const gMetaData: BaseMetaData = {
  severity: 2,
  ruleDocPath: "docs/no-extra-non-null-assertion.md",
  description: "Disallow extra non-null assertions",
};

export class NoExtraNonNullAssertionCheck implements AdviceChecker {
  readonly metaData: BaseMetaData = gMetaData;
  public rule: Rule;
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  private issueMap: Map<string, IssueReport> = new Map();

  private buildMatcher: AstMatcher = {
      matcherType: MatcherTypes.AST,
      match(node: ts.Node): boolean {
          return ts.isNonNullExpression(node);
      }
  };

  public registerMatchers(): MatcherCallback[] {
    const matchBuildCb: MatcherCallback = {
        matcher: this.buildMatcher,
        callback: this.check
    };
    return [matchBuildCb];
  };

  public beforeCheck(): void {}

  public afterCheck(): void {
    this.defects = [];
    this.issueMap.clear();
  }

  public check = (node: ts.NonNullExpression, target: ArkFile): void => {
    const result = this.analyzeNonNullExpression(node);
    if (!result.redundant) {
      return;
    }

    const sourceFile = node.getSourceFile();
    const start = sourceFile.getLineAndCharacterOfPosition(result.start);
    const end = sourceFile.getLineAndCharacterOfPosition(result.end);

    this.addIssueReport(node, target, start.line + 1, start.character + 1, end.character + 1, result.end);
    this.reportSortedIssues();
  };

  private analyzeNonNullExpression(node: ts.NonNullExpression): { redundant: boolean; start: number; end: number } {
    const start = node.getStart();
    const end = node.getEnd();

    // 检查多重断言，如 foo!!!
    if (ts.isNonNullExpression(node.expression)) {
      return { redundant: true, start, end };
    }
    // 检查形如 (a!)! 的括号包裹式双断言。
    if (
      ts.isParenthesizedExpression(node.expression) &&
      ts.isNonNullExpression(node.expression.expression)
    ) {
      const inner = node.expression.expression;
      return { redundant: true, start: inner.getStart(), end: inner.getEnd() };
    }
    // 检查属性或方法调用中的多余断言bar!?.()
    const parent = node.parent;
    if (
      (ts.isPropertyAccessExpression(parent) && parent.questionDotToken) ||
      (ts.isCallExpression(parent) && parent.questionDotToken) ||
      (ts.isParenthesizedExpression(parent) && (
        (ts.isPropertyAccessExpression(parent.parent) && parent.parent.questionDotToken) ||
        (ts.isCallExpression(parent.parent) && parent.parent.questionDotToken)
      )) ||
      (ts.isElementAccessExpression(parent) && 
       parent.questionDotToken && 
       parent.expression === node)
    ) {
      return { redundant: true, start, end };
    }

    return { redundant: false, start, end };
  }

  private addIssueReport(node: ts.NonNullExpression, file: ArkFile, startLine: number, startCol: number, endCol: number, endPos: number): void {
    const message = 'Forbidden extra non-null assertion.';
    const severity = this.rule.alert ?? this.metaData.severity;
    const issueKey = `${startLine}%${startCol}%${endCol}%${this.rule.ruleId}`;
    const defect = new Defects(startLine, startCol, endCol, message, severity, 
      this.rule.ruleId, file.getFilePath(), this.metaData.ruleDocPath, true, false, true);

    const fix: RuleFix = { range: [endPos - 1, endPos], text: '' };

    this.defects.push(defect);
    this.issueMap.set(issueKey, { defect, fix });
  }

  private reportSortedIssues(): void {
    if (this.issueMap.size === 0) {
      return;
    }

    const sortedIssues = Array.from(this.issueMap.entries())
      .sort(([keyA], [keyB]) => {
        const [lineA, colA] = keyA.split('%');
        const [lineB, colB] = keyB.split('%');
        if (lineA !== lineB) {
          return Number(lineA) - Number(lineB);
        }
        return Number(colA) - Number(colB);
      });

    this.issues = [];
    sortedIssues.forEach(([_, issue]) => {
      RuleListUtil.push(issue.defect);
      this.issues.push(issue);
    });
  }
}
